跳到主要内容

JavaScript 事件机制

事件机制

系统会在事件出现时产生或触发某种信号,并且会提供一个自动加载某种动作(列如:运行一些代码)的机制

每个可用的事件都会有一个事件处理器,也就是事件触发时会运行的代码块。注意事件处理器有时候被叫做 事件监听器 :从我们的用意来看这两个名字是相同的,尽管严格地来说这块代码既监听也处理事件。监听器留意事件是否发生,然后处理器就是对事件发生做出的回应。

Javascript 事件循环

既然 js 是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理 js 任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。

那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

  • 同步任务
  • 异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

该单线程的执行流程如下:

导图要表达的内容用文字来表述的话:

同步和异步任务分别进入不同的执行 "场所",同步的进入主线程,异步的进入 Event Table 并注册函数。

当指定的事情完成时,Event Table 会将这个函数移入Event Queue。

主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。

上述过程会不断重复,也就是常说的 Event Loop(事件循环)。

我们不禁要问了,那怎么知道主线程执行栈为空啊?JS 引擎存在 monitoring process 进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里检查是否有等待被调用的函数。

牢记:JavaScript 是单线程,同一时间只能做一件事情(注意!异步也只是将代码的执行顺序滞后而已)

例如下面代码的执行顺序就是一个微任务和宏任务执行顺序不同的体现

setTimeout(() => {
console.log(2);
}, 0);

// 注意别忘了 Promise 的构造函数还是同步的,then才是异步任务
new Promise(resolve => {
console.log(3);
resolve();
console.log(4);
}).then(()=>{
console.log(5);
});

console.log(8);

/*
执行结果
3
4
8
5
2
*/

什么是宏任务和微任务

哪些是宏任务哪些是微任务

宿主环境(浏览器、node)提供的方法是宏任务,例如 setTimeoutsetInterval 语言标准(js引擎)提供的是微任务,例如 Promise

具体如下:

宏任务:script(用户交互事件)setTimeoutsetIntervalsetImmediateI/OUI rendering(渲染事件) 微任务:PromiseObject.observeMutationObserver

微任务和宏任务的执行

微任务 宏任务都是异步任务;但是它们的执行时机是有区别的,如下图所示要先执行完任务列表的微任务才能执行下一个宏任务

UXyfsI.png

当同步任务执行完成,依次执行微任务队列中的所有微任务。执行完所有微任务后,从宏任务队列中获取新的宏任务执行。这样就完成了一个事件循环。

看下图,每个宏任务后面跟着一个微任务链表

image.png

如上图:当执行同步任务遇到一个异步任务时,就在 event table(事件表)中注册回调函数,同步任务继续执行。期间异步任务完成时,回调函数会被放入 event queue(事件队列)。

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。

每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为 null,然后再执行宏任务,如此循环。

任务的优先级

process.nextTick > promise.then > setTimeout > setImmediate

微任务和宏任务的例子

如下也可以看到微任务是如何挂载在宏任务上面的

输出结果:

1 7 6 8 2 4 3 5 9 11 10 12

为什么要区分微任务和宏任务?

区分微任务和宏任务是为了将异步队列任务划分优先级,通俗的理解就是为了插队。

首先,一个前提是宏任务必须存在,这个应该比较好理解,比如渲染事件、用户交互事件这些都属于宏任务。

那么问题就成了,为什么JS需要有微任务?一个比较好的说法是,微任务是效率和实时性之间权衡的产物。

当没有微任务的情况下: 1、效率:如果一个任务的时间越长,那么任务的执行效率就越低下。当我们希望执行一个新的任务,但又不希望它影响到当前任务的执行效率,最好的方法就是将它推到宏任务队列的尾部。

2、实时性:当我们使用 setTimeout 的时候,浏览器会在适当的时机去将这个任务推到任务队列的尾部,但开发者无法控制到底什么时候 setTimeout 中的内容会被执行,因为不确定浏览器会在这中间插入多少个宏任务,这时候实时性就会出现问题。

因此,实时性和效率难以两全,微任务应运而生。

当微任务存在时,开发者在 js 执行过程中插入了一个新的微任务,不会延长当前宏任务的执行时间,效率也不会被降低。同时,执行完当前宏任务后,不会马上执行下一个宏任务,而是执行微任务,那么实时性就得到了一定的保证。

NodeJS 的事件是怎么样的?

参考资料 NodeJS中文文档--事件触发器

Node.js 使用像 on() 这样的函数来注册一个事件监听器,使用 once() 这样函数来注册一个在运行一次之后注销的监听器

使用例

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('触发事件');
});


myEmitter.emit('event');

详细看下面

Web 事件机制

默认的 DOM 的事件

参考自MDN--事件参考

<button>Change color</button>
var btn = document.querySelector('button');

function random(number) {
return Math.floor(Math.random()*(number+1));
}

btn.onclick = function() {
var rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
}

注册自定义事件

参考资料 创建和触发 events

Events 可以使用 Event 构造函数创建如下

window.dispatchEvent(new CustomEvent("refreshData")); // 自定义一个刷新事件

// 定义一个刷新事件的监听
window.addEventListener("refreshData", () => {
// 这里进行刷新操作
});

NodeJS的事件机制

参考资料 NodeJS中文文档--事件触发器 参考资料 MDN--事件介绍

事件是在编程时系统内发生的动作或者发生的事情;即 系统会在事件出现时产生或触发某种信号,并且会提供一个自动加载某种动作(列如:运行一些代码)的机制

注册事件

Node.js 使用像 on() 这样的函数来注册一个事件监听器,使用 once() 这样函数来注册一个在运行一次之后注销的监听器

使用例

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('触发事件');
});


myEmitter.emit('event');

移除事件监听

参考资料 中文文档

removeListener(event, listener) 移除指定事件的某个监听器。不过会在触发的监听事件执行完成后才移除

var callback = function(stream) {
console.log('someone connected!');
};

server.on('connection', callback);
// ...
server.removeListener('connection', callback);

Reference